package org.erikaredmark.monkeyshines.animation; import java.awt.AlphaComposite; import java.awt.BasicStroke; import java.awt.Color; import java.awt.Graphics2D; import java.awt.image.BufferedImage; import org.erikaredmark.monkeyshines.Bonzo; import org.erikaredmark.monkeyshines.GameConstants; import org.erikaredmark.monkeyshines.resource.CoreResource; /** * * State-based animation for when bonzo dies and time must be given to show where he is respawning. It is up to renderers to * decide when to fire the grace period animation. * <p/> * The animation is designed such that it does not steal control from the main renderer. The renderer will operate at the * same speed as always and will fire this objects paint method instead of the main one for drawing the universe. Each call * to paint will render the next frame until all frames are rendered and paint returns 'false' for no more frames, in which * the client should then do what is required to allow this object to be gc'd. * <p/> * The main purpose is simply to encapsulate an animation state machine. All rendering takes place in addition to any other * rendering the system chooses to make. For example, it is still the responsibility of the client to render the actual world * with bonzo on it; this renders the circle and get-ready text only. * * @author Erika Redmark * */ public final class GracePeriodAnimation { /** * * Constructs a new animation for the grace period. This animation will be allowed to run for 'frames' * number of frames before no longer working. * * @param Bonzo * reference to bonzo. His current location will be used to set the animation, but further changes * to his state will NOT affect the animation. * * @param frames * number of frames this animation should last. * * @param xOffset * xOffset for bonzos location. This is if there are UI elements to either the left or right of where the playable * area is on the current graphics context. * * @param yOffset * yOffset for bonzos location. This is if there are UI elements to either the top or bottom of where * the playable area is on the current graphics context. * */ public GracePeriodAnimation(Bonzo bonzo, int frames, int xOffset, int yOffset) { // final, constant parameters basic on data centerX = (bonzo.getCurrentLocation().x() + 20) + xOffset; centerY = (bonzo.getCurrentLocation().y() + 20) + yOffset; maxRadius = 300; minRadius = 30; radiusStep = ((double)(maxRadius - minRadius)) / (double)frames; assert radiusStep > 0 : "Infinite Loop: Radius steps must be positive non-zero values"; opacityInitial = 1.0f; opacityFinal = 0.2f; opacityStep = (opacityFinal - opacityInitial) / (float)frames; BufferedImage getReady = CoreResource.INSTANCE.getGetReady(); getReadyX = (GameConstants.SCREEN_WIDTH / 2) - (getReady.getWidth() / 2); getReadyY = (GameConstants.SCREEN_HEIGHT / 2) - (getReady.getHeight() / 2); getReadyX2 = getReadyX + getReady.getWidth(); getReadyY2 = getReadyY + getReady.getHeight(); // Mutable state data initialisation currentRadius = maxRadius; currentOpacity = opacityInitial; } /** * * Paints the current frame of animation. * <p/> * Do not ignore the return value of updating. Animating after a false return can cause unexpected behaviour. * <p/> * Note: The g2d is mutated with a new stroke, colour, and composite; be sure to save and restore those values if * required. * * @param g2d * * @return * */ public void paint(Graphics2D g2d) { // Animation: Start circle at low opacity from center of bonzo, at a good radius, then decrease radius and increase opacity // at rates such that by 'ms' time, circle if fully opaque and surrounds Bonzo directly. g2d.setStroke(new BasicStroke(4) ); g2d.setColor(Color.DARK_GRAY); g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, currentOpacity) ); BufferedImage getReady = CoreResource.INSTANCE.getGetReady(); g2d.drawImage(getReady, getReadyX, getReadyY, getReadyX2, getReadyY2, 0, 0, getReady.getWidth(), getReady.getHeight(), null); g2d.drawOval((int)(centerX - 2), (int)(centerY - 2), 4, 4); g2d.drawOval((int)(centerX - currentRadius), (int)(centerY - currentRadius), (int)(currentRadius*2), (int)(currentRadius*2) ); } /** * * Updates the animation to the next frame, returning if there are no more animation frames. * * @return * If this was the last frame for this object, returns {@code false}, * otherwise returns {@code true} */ public boolean update() { currentRadius -= radiusStep; currentOpacity += opacityStep; if (currentRadius > minRadius) return true; else return false; } // Immutable data final int centerX; final int centerY; final int maxRadius; final int minRadius; final double radiusStep; final float opacityInitial; final float opacityFinal; final float opacityStep; final int getReadyX; final int getReadyY; final int getReadyX2; final int getReadyY2; // State data double currentRadius; float currentOpacity; }